/*
 * Copyright 2010 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.anim.impl
{
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	
	import sweezy.anim.core.IAction;
	import sweezy.anim.core.IAnimation;
	import sweezy.anim.core.IAnimationDecorator;
	import sweezy.anim.core.IAnimationGroup;
	import sweezy.anim.core.IAnimationManager;
	import sweezy.anim.core.IEasing;
	import sweezy.anim.core.IPhisycalEasing;
	import sweezy.anim.core.IPropertyAnimation;
	import sweezy.anim.core.ITimer;
	import sweezy.anim.core.ITimerClient;
	import sweezy.anim.core.IUpdater;
	import sweezy.anim.easing.Easing;
	import sweezy.anim.easing.Linear;
	import sweezy.anim.easing.PhisycalEasing;
	import sweezy.anim.timer.EnterFrameEventTimer;
	import sweezy.anim.timer.FrameTimer;

	public class AnimationManager implements IAnimationManager, ITimerClient
	{

		private static var _restractWords:Object = {
				"option": null,
				"autoRewind": null,
				"onComplete": null,
				"onCompleteParams": null,
				"onCompleteScope": null,
				"onStart": null,
				"onStartParams": null,
				"onStartScope": null,
				"onUpdate": null,
				"onUpdateParams": null,
				"onUpdateScope": null,
				"onPause": null,
				"onPauseParams": null,
				"onPauseScope": null,
				"onError": null,
				"onErrorScope": null,
				"duration": null,
				"delay": null,
				"easing": null
			};

		internal var _count:uint;

		private var _defaultDuration:uint;

		private var _defaultEasing:IEasing;

		private var _head:AnimationBase;

		private var _tail:AnimationBase;

		private var _timer:ITimer;

		private var _updaterByElementType:Dictionary;

		private var _updaterByPrefix:Object;

		private var _updaterCache:Object;

		public function AnimationManager()
		{
			_defaultEasing = Linear.easeNone;
			_defaultDuration = 500;
			_updaterByPrefix = {};
			_updaterByElementType = new Dictionary();
			_updaterCache = {};
			_count = 0;

			_head = new DummyAnimation();
			_tail = new DummyAnimation();
			_head._next = _tail;
			_tail._previous = _head;
			_tail._next = _tail;

			Updaters.init(this);
			setTimer(new FrameTimer());
		}

		public function createActionAnimation(animation:IAnimation, action:IAction, quePosition:uint):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new ActionAnimation(this, animation as AnimationBase, action, quePosition);
		}

		public function createEasingAnimation(animation:IAnimation, easing:IEasing):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new EasingAnimation(this, animation as AnimationBase, easing);
		}

		public function createEmptyAnimation(duration:uint):IAnimation
		{
			return new EmptyAnimation(this, duration);
		}


		public function createPaddingAnimation(animation:IAnimation, before:uint, after:uint):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new PaddingAnimation(this, animation as AnimationBase, before, after);
		}

		public function createParallelAnimation(animations:Array):IAnimationGroup
		{
			if (animations !== null)
			{
				animations = animations.filter(notNull);
			}
			if (animations === null || animations.length === 0)
			{
				return null;
			}

			return new ParallelAnimation(this, animations);
		}

		public function createPhisycalEasingAnimation(element:Object, easing:IPhisycalEasing, delay:uint):IPropertyAnimation
		{
			if (element === null)
			{
				return null;
			}

			if (easing === null)
			{
				easing = PhisycalEasing.uniform();
			}
			return new PropertyAnimation(this, element, 0, easing, delay);
		}

		public function createPropertyAnimation(element:Object, duration:uint, easing:IEasing, delay:uint):IPropertyAnimation
		{
			if (element === null)
			{
				return null;
			}

			return new PropertyAnimation(this, element, duration, easing, delay);
		}

		public function createPropertyAnimationByProperties(element:Object, properties:Object):IPropertyAnimation
		{
			if (element === null)
			{
				return null;
			}
			if (properties === null)
			{
				return createPropertyAnimation(element, 0, null, 0);
			}

			properties = preprocessProperties(properties);
			var duration:uint = 0;
			var delay:uint = 0;
			var easing:Object = null;

			if ("duration" in properties)
			{
				duration = uint(properties.duration);
			}
			if ("delay" in properties)
			{
				delay = uint(properties.delay);
			}
			if ("easing" in properties)
			{
				easing = properties.easing;
				if (easing is String)
				{
					easing = Easing.ease(easing as String);
				}
				else if (easing is Function)
				{
					easing = Easing.convert(easing);
				}
				else if (!(easing is IEasing))
				{
					easing = null;
				}
			}

			var anim:IPropertyAnimation = createPropertyAnimation(element, duration, easing as IEasing, delay);

			("option" in properties) && (anim.option = properties.option);
			("autoRewind" in properties) && (anim.autoRewind = Boolean(properties.autoRewind));
			("onComplete" in properties) && (anim.onComplete = properties.onComplete as Function);
			("onCompleteParams" in properties) && (anim.onCompleteParams = properties.onCompleteParams as Array);
			("onCompleteScope" in properties) && (anim.onCompleteScope = properties.onCompleteScope);
			("onStart" in properties) && (anim.onStart = properties.onStart as Function);
			("onStartParams" in properties) && (anim.onStartParams = properties.onStartParams as Array);
			("onStartScope" in properties) && (anim.onStartScope = properties.onStartScope);
			("onUpdate" in properties) && (anim.onUpdate = properties.onUpdate as Function);
			("onUpdateParams" in properties) && (anim.onUpdateParams = properties.onUpdateParams as Array);
			("onUpdateScope" in properties) && (anim.onUpdateScope = properties.onUpdateScope);
			("onPause" in properties) && (anim.onPause = properties.onPause as Function);
			("onPauseParams" in properties) && (anim.onPauseParams = properties.onPauseParams as Array);
			("onPauseScope" in properties) && (anim.onPauseScope = properties.onPauseScope);
			("onError" in properties) && (anim.onError = properties.onError as Function);
			("onErrorScope" in properties) && (anim.onErrorScope = properties.onErrorScope);

			for (var name:String in properties)
			{
				if (name in _restractWords)
				{
					continue;
				}

				var value:Object = properties[name];
				if (value === null)
				{
					continue;
				}

				anim.to(name, Number(value));
			}

			return anim;
		}

		public function createRepeatAnimation(animation:IAnimation, numRepeat:uint):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new RepeatAnimation(this, animation as AnimationBase, numRepeat);
		}

		public function createReverseAnimation(animation:IAnimation):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new ReverseAnimation(this, animation as AnimationBase);
		}

		public function createRoundTripAnimation(animation:IAnimation):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new RoundTripAnimation(this, animation as AnimationBase);
		}

		public function createScaledAnimation(animation:IAnimation, scale:Number):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new ScaleAnimation(this, animation as AnimationBase, scale);
		}

		public function createSerialAnimation(animations:Array):IAnimationGroup
		{
			if (animations !== null)
			{
				animations = animations.filter(notNull);
			}
			if (animations === null || animations.length === 0)
			{
				return null;
			}

			return new SerialAnimation(this, animations);
		}

		public function createSlicedAnimation(animation:IAnimation, startPosition:uint, sliceDuration:uint):IAnimationDecorator
		{
			if (animation === null)
			{
				return null;
			}

			return new SliceAnimation(this, animation as AnimationBase, startPosition, sliceDuration);
		}

		public function get defaultDuration():uint
		{
			return _defaultDuration;
		}

		public function set defaultDuration(value:uint):void
		{
			_defaultDuration = value === 0 ? 500 : value;
		}

		public function get defaultEasing():IEasing
		{
			return _defaultEasing;
		}

		public function set defaultEasing(value:IEasing):void
		{
			_defaultEasing = value;
		}

		public function deregisterUpdaterByElementType(elementType:Class):void
		{
			if (elementType === null)
			{
				return;
			}

			_updaterCache = {};
			delete _updaterByElementType[elementType];
		}

		public function deregisterUpdaterByPrefix(prefix:String):void
		{
			if (prefix === null)
			{
				return;
			}

			delete _updaterByPrefix[prefix];
		}

		public function getRelatedAnimations(element:Object):Array
		{
			if (element === null)
			{
				return [];
			}

			var animation:AnimationBase = _head._next;
			var count:int = _count;
			var animations:Dictionary = new Dictionary();

			while (--count > -1)
			{
				getRelatedAnimations0(animation, element, animations);
				animation = animation._next;
			}

			var result:Array = [];
			var i:int = 0;
			for (var anim:Object in animations)
			{
				result[i++] = anim;
			}

			return result;
		}

		public function getUpdaterByElement(element:Object):IUpdater
		{
			var name:String = getQualifiedClassName(element);
			if (name in _updaterCache)
			{
				return _updaterCache[name];
			}

			for (var type:Object in _updaterByElementType)
			{
				if (element is (type as Class))
				{
					_updaterCache[name] = _updaterByElementType[type];
					return _updaterByElementType[type];
				}
			}

			return null;
		}

		public function getUpdaterByPrefix(prefix:String):IUpdater
		{
			if (prefix === null)
			{
				return null;
			}

			prefix = prefix.toLocaleLowerCase();
			if (prefix in _updaterByPrefix)
			{
				return _updaterByPrefix[prefix];
			}

			return null;
		}

		public function pauseRelatedAnimations(element:Object):void
		{
			for each (var animation:IAnimation in getRelatedAnimations(element))
			{
				animation.pause();
			}
		}

		public function registerUpdaterByElementType(elementType:Class, updater:IUpdater):void
		{
			if (elementType === null || updater === null)
			{
				return;
			}

			_updaterByElementType[elementType] = updater;
		}

		public function registerUpdaterByPrefix(prefix:String, updater:IUpdater):void
		{
			if (prefix === null || updater === null)
			{
				return;
			}

			_updaterByPrefix[prefix.toLocaleLowerCase()] = updater;
		}

		public function setTimer(timer:ITimer):void
		{
			if (timer === null)
			{
				return;
			}

			if (_timer !== null && _timer.initialized)
			{
				_timer.destroy();
			}
			if (timer.initialized)
			{
				timer.destroy();
			}
			timer.initialize(this);
			_timer = timer;
		}

		public function startTimer(animation:AnimationBase):void
		{
			if ((animation._state & 0x04) === 0x04)
			{
				animation._state &= ~0x02;
			}
			else
			{

				_tail._previous._next = animation;
				animation._previous = _tail._previous;
				animation._next = _tail;
				_tail._previous = animation;

				animation._state |= 0x04;
				if (++_count === 1)
				{
					_timer.record();
				}
			}

			var time:int = _timer.time;
			animation._delta = animation._position - time;
			animation.updateFromTimer(time);
		}

		public function tick(time:int):void
		{
			if (_count === 0)
			{
				return;
			}

			var animation:AnimationBase = _head._next;
			var remain:int = (_count + 7) >> 3;

			while (--remain > -1)
			{
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
				animation = animation.updateFromTimer(time);
			}
		}

		public function updateNow():void
		{
			tick(_timer.record());
		}

		private function getRelatedAnimations0(animation:IAnimation, element:Object, animations:Dictionary):void
		{
			if (animation is PropertyAnimation)
			{
				if ((animations as PropertyAnimation)._element === element)
				{
					animations[animation] = null;
				}

				return;
			}

			if (animation is AnimationGroupBase)
			{
				var anims:Array = (animation as AnimationGroupBase).animations;
				for (var i:int = 0, len:uint = anims.length; i < len; i++)
				{
					getRelatedAnimations0(anims[i], element, animations);
				}

				return;
			}

			if (animation is AnimationDecoratorBase)
			{
				getRelatedAnimations0((animation as AnimationDecoratorBase).targetAnimation, element, animations);
				return;
			}
		}

		private function notNull(element:AnimationBase, index:uint, array:Array):Boolean
		{
			return element !== null;
		}

		private function preprocessProperties(properties:Object):Object
		{
			var base:Object = properties.base;
			if (base === null)
			{
				return properties;
			}

			var result:Object = {};
			var name:String;

			for (name in properties)
			{
				if (name !== "base")
				{
					result[name] = properties[name];
				}
			}

			if (base is Array)
			{
				var bases:Array = base as Array;
				for (var i:int = 0, len:uint = bases.length; i < len; i++)
				{
					properties = preprocessProperties(bases[i]);
					for (name in properties)
					{
						if (name !== "base" && !(name in result))
						{
							result[name] = properties[name];
						}
					}
				}
			}
			else
			{
				properties = preprocessProperties(base);
				for (name in properties)
				{
					if (name !== "base" && !(name in result))
					{
						result[name] = properties[name];
					}
				}
			}

			return result;
		}
	}
}
